﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace QQ2564874169.Core.ORM
{
    public interface ISql : IDbSqlQuery
    {
        int Execute(string sql, object param = null, bool isProc = false, int? timeout = null);
    }

    public abstract class Sql : ISql
    {
        public Hashtable Data { get; }
        public bool InTransaction => _tcount > 0;
        public static event Action<Sql> Created; 
        public event EventHandler<SqlBeforeEventArgs> Before;
        public event EventHandler<SqlAfterEventArgs> After;
        public event EventHandler<SqlException> Error;
        public event EventHandler<TransactionFinishEventArgs> TransactionBefore;
        public event EventHandler<TransactionFinishEventArgs> TransactionAfter;
        public string Id { get; }

        private int _tcount;
        private int _tlevel;

        protected Sql()
        {
            Id = Guid.NewGuid().ToString();
            Data = new Hashtable();
            Created?.Invoke(this);
        }

        public void Dispose()
        {
            Rollback();
            OnDispose();
            Data?.Clear();
        }

        protected abstract void OnDispose();

        protected virtual SqlBeforeEventArgs OnBefore(SqlContext context)
        {
            var args = new SqlBeforeEventArgs
            {
                Context = context
            };
            Before?.Invoke(this, args);
            return args;
        }

        protected virtual void OnAfter(SqlBeforeEventArgs before)
        {
            var args = new SqlAfterEventArgs
            {
                Context = before.Context,
                Result = before.Result
            };
            After?.Invoke(this, args);
        }

        protected virtual void OnError(SqlContext context, Exception ex)
        {
            Error?.Invoke(this, new SqlException {Context = context, Error = ex});
        }

        protected virtual void OnTransactionBefore(bool isCommited)
        {
            TransactionBefore?.Invoke(this, new TransactionFinishEventArgs(isCommited));
        }

        protected virtual void OnTransactionAfter(bool isCommited)
        {
            TransactionAfter?.Invoke(this, new TransactionFinishEventArgs(isCommited));
        }

        protected abstract int OnExecute(SqlContext context);

        protected abstract T OnQueryOne<T>(SqlContext context);

        protected abstract IEnumerable<T> OnQuery<T>(SqlContext context);

        protected abstract IMultipleReader OnMultiple(SqlContext context);

        protected abstract PageResult<T> OnPager<T>(SqlContext context, PageSet pageset);

        protected abstract void OnBeginTransaction();

        protected abstract void OnCommitTransaction();

        protected abstract void OnRollbackTransaction();

        protected virtual ITransactionScope OnCreateTransactionScope()
        {
            return new TransactionScope();
        }

        private static void ExpandIn(SqlContext context)
        {
            if (context.Param == null || context.IsProc)
            {
                return;
            }

            foreach (var item in context.Param.ToArray())
            {
                var list = ToList(item.Value);
                if (list == null || context.Command.Contains("@" + item.Key) == false)
                {
                    continue;
                }

                var ns = new List<string>();
                for (var i = 0; i < list.Count; i++)
                {
                    var name = item.Key + "_" + i;
                    ns.Add("@" + name);
                    context.Param.Add(name, list[i]);
                }

                var sql = $"({string.Join(",", ns)})";
                context.Command = context.Command.Replace(" @" + item.Key, sql);
                context.Param.Remove(item.Key);
            }
        }

        private static List<object> ToList(object value)
        {
            if (value is string)
            {
                return null;
            }
            if (value is ICollection c)
            {
                var rs = new List<object>();
                foreach (var item in c)
                {
                    rs.Add(item);
                }

                return rs;
            }
            if (value is IEnumerable e)
            {
                var rs = new List<object>();
                foreach (var item in e)
                {
                    rs.Add(item);
                }
                return rs;
            }

            return null;
        }

        public int Execute(string sql, object param = null, bool isProc = false, int? timeout = null)
        {
            CheckExTransaction();
            var context = new SqlContext
            {
                Command = sql,
                IsProc = isProc,
                Param = ParseParam(param),
                Timeout = timeout,
                Type = OpType.Execute
            };
            try
            {
                var args = OnBefore(context);
                ExpandIn(context);
                args.Result = args.Result ?? OnExecute(context);
                OnAfter(args);
                return (int) args.Result;
            }
            catch (Exception ex)
            {
                OnError(context, ex);
                throw;
            }
        }

        public T QueryOne<T>(string sql, object param = null, bool isProc = false, int? timeout = null)
        {
            CheckExTransaction();
            var context = new SqlContext
            {
                Command = sql,
                IsProc = isProc,
                Param = ParseParam(param),
                Timeout = timeout,
                Type = OpType.QueryOne
            };
            try
            {
                var args = OnBefore(context);
                ExpandIn(context);
                args.Result = args.Result ?? OnQueryOne<T>(context);
                OnAfter(args);
                return (T) args.Result;
            }
            catch (Exception ex)
            {
                OnError(context, ex);
                throw;
            }
        }

        public IEnumerable<T> Query<T>(string sql, object param = null, bool isProc = false, int? timeout = null)
        {
            CheckExTransaction();
            var context = new SqlContext
            {
                Command = sql,
                IsProc = isProc,
                Param = ParseParam(param),
                Timeout = timeout,
                Type = OpType.Query
            };
            try 
            {
                var args = OnBefore(context);
                ExpandIn(context);
                args.Result = args.Result ?? OnQuery<T>(context);
                OnAfter(args);
                return (IEnumerable<T>) args.Result;
            }
            catch (Exception ex)
            {
                OnError(context, ex);
                throw;
            }
        }

        public IMultipleReader Multiple(string sql, object param = null, bool isProc = false, int? timeout = null)
        {
            CheckExTransaction();
            var context = new SqlContext
            {
                Command = sql,
                IsProc = isProc,
                Param = ParseParam(param),
                Timeout = timeout,
                Type = OpType.Multiple
            };
            try
            {
                var args = OnBefore(context);
                ExpandIn(context);
                args.Result = args.Result ?? OnMultiple(context);
                var reader = (IMultipleReader) args.Result;
                reader.Disposed += (s, e) =>
                {
                    OnAfter(args);
                };
                return reader;
            }
            catch (Exception ex)
            {
                OnError(context, ex);
                throw;
            }
        }

        public PageResult<T> Pager<T>(PageSet pageset, string sql, object param = null, bool isProc = false, int? timeout = null)
        {
            CheckExTransaction();
            var context = new SqlContext
            {
                Command = sql,
                IsProc = isProc,
                Param = ParseParam(param),
                Timeout = timeout,
                Type = OpType.Pager
            };
            try
            {
                var args = OnBefore(context);
                ExpandIn(context);
                args.Result = args.Result ?? OnPager<T>(context, pageset);
                OnAfter(args);
                return (PageResult<T>)args.Result;
            }
            catch (Exception ex)
            {
                OnError(context, ex);
                throw;
            }
        }

        private void CheckExTransaction()
        {
            if (InTransaction == false && _tlevel > 0)
            {
                throw new Exception("事务已回滚，对象在事务完全退出前无法再使用。");
            }
        }

        private void Rollback()
        {
            if (InTransaction == false)
                return;
            OnTransactionBefore(false);
            OnRollbackTransaction();
            _tcount = 0;
            OnTransactionAfter(false);
        }

        private void Commit()
        {
            if (InTransaction == false)
                return;
            OnTransactionBefore(true);
            OnCommitTransaction();
            _tcount = 0;
            OnTransactionAfter(true);
        }

        public ITransactionScope Transaction()
        {
            if (_tcount++ < 1)
            {
                OnBeginTransaction();
            }
            var scope = OnCreateTransactionScope();

            scope.Finish += (s, e) =>
            {
                if (e.IsCommited)
                {
                    if (_tcount == 0)
                    {
                        throw new Exception("当前事务已终止");
                    }
                    if (_tcount == 1)
                    {
                        Commit();
                    }
                    else
                    {
                        _tcount--;
                    }
                }
                else
                {
                    Rollback();
                }

                _tlevel--;
            };
            _tlevel++;
            return scope;
        }

        protected virtual IDictionary<string, object> ParseParam(object param)
        {
            var dict = new Dictionary<string, object>();

            if (param == null)
                return dict;

            if (param is IDictionary<string, object> rs)
            {
                return rs;
            }

            if (param is IDictionary map)
            {
                foreach (var key in map.Keys)
                {
                    dict.Add(key.ToString(), map[key]);
                }

                return dict.Count > 0 ? dict : null;
            }

            foreach (var p in param.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                var v = p.GetValue(param, null);
                if (v != null)
                {
                    dict.Add(p.Name, v);
                }
            }

            return dict.Count > 0 ? dict : null;
        }
    }
}
